/**********************************************************************

  Audacity: A Digital Audio Editor

  TruncSilenceBase.cpp

  Lynn Allan (from DM's Normalize)
  Philip Van Baren (more options and boundary fixes)

*******************************************************************//**

\class TruncSilenceBase
\brief Truncate Silence automatically reduces the length of passages
       where the volume is below a set threshold level.

*//*******************************************************************/
#include "TruncSilenceBase.h"
#include "BasicUI.h"
#include "EffectOutputTracks.h"
#include "Prefs.h"
#include "Project.h"
#include "WaveTrack.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <list>

class Enums
{
public:
    static const size_t NumDbChoices;
    static const EnumValueSymbol DbChoices[];
};

const EnumValueSymbol Enums::DbChoices[] = {
    // Table of text values, only for reading what was stored in legacy config
    // files.
    // It was inappropriate to make this a discrete choice control.
    { wxT("-20 dB") }, { wxT("-25 dB") }, { wxT("-30 dB") }, { wxT("-35 dB") },
    { wxT("-40 dB") }, { wxT("-45 dB") }, { wxT("-50 dB") }, { wxT("-55 dB") },
    { wxT("-60 dB") }, { wxT("-65 dB") }, { wxT("-70 dB") }, { wxT("-75 dB") },
    { wxT("-80 dB") }
};

// Map from position in table above to numerical value.
static inline double enumToDB(int val)
{
    return -(5.0 * val + 20.0);
}

const size_t Enums::NumDbChoices = WXSIZEOF(Enums::DbChoices);

using Region = WaveTrack::Region;

// Declaration of RegionList
class RegionList : public std::list<Region>
{
};

const EnumValueSymbol TruncSilenceBase::kActionStrings[nActions] = {
    { XO("Truncate Detected Silence") }, { XO("Compress Excess Silence") }
};

static CommandParameters::ObsoleteMap kObsoleteActions[] = {
    // Compatible with 2.1.0 and before
    { wxT("0"), 0 }, // Remap to Truncate Detected Silence
    { wxT("1"), 1 }, // Remap to Compress Excess Silence
};

static const size_t nObsoleteActions = WXSIZEOF(kObsoleteActions);

const EffectParameterMethods& TruncSilenceBase::Parameters() const
{
    static CapturedParameters<
        TruncSilenceBase, Threshold, ActIndex, Minimum, Truncate, Compress,
        Independent>
    parameters;
    return parameters;
}

static const size_t DEF_BlendFrameCount = 100;

// Lower bound on the amount of silence to find at a time -- this avoids
// detecting silence repeatedly in low-frequency sounds.
static const double DEF_MinTruncMs = 0.001;

// Typical fraction of total time taken by detection (better to guess low)
const double detectFrac = 0.4;

const ComponentInterfaceSymbol TruncSilenceBase::Symbol { XO(
                                                              "Truncate Silence") };

TruncSilenceBase::TruncSilenceBase()
{
    Parameters().Reset(*this);

    SetLinearEffectFlag(false);

    // This used to be changeable via the audacity.cfg/registry.  Doubtful that
    // was ever done.
    //
    // Original comment:
    //
    //   mBlendFrameCount only retrieved from prefs ... not using dialog
    //   Only way to change (for windows) is thru registry
    //   The values should be figured dynamically ... too many frames could be
    //   invalid
    mBlendFrameCount = DEF_BlendFrameCount;
}

TruncSilenceBase::~TruncSilenceBase()
{
}

// ComponentInterface implementation

ComponentInterfaceSymbol TruncSilenceBase::GetSymbol() const
{
    return Symbol;
}

TranslatableString TruncSilenceBase::GetDescription() const
{
    return XO(
        "Automatically reduces the length of passages where the volume is below a specified level");
}

ManualPageID TruncSilenceBase::ManualPage() const
{
    return L"Truncate_Silence";
}

// EffectDefinitionInterface implementation

EffectType TruncSilenceBase::GetType() const
{
    return EffectTypeProcess;
}

bool TruncSilenceBase::LoadSettings(
    const CommandParameters& parms, EffectSettings& settings) const
{
    Effect::LoadSettings(parms, settings);

    // A bit of special treatment for two parameters

    // This control migrated from a choice to a text box in version 2.3.0
    double myThreshold {};
    bool newParams = [&] {
        double temp;
        if (!parms.ReadAndVerify(
                Threshold.key, &temp, Threshold.def, Threshold.min, Threshold.max)) {
            return false;
        }
        myThreshold = temp;
        return true;
    }();

    if (!newParams) {
        int temp;
        // Use legacy param:
        if (!parms.ReadAndVerify(
                L"Db", &temp, 0, Enums::DbChoices, Enums::NumDbChoices)) {
            return false;
        }
        myThreshold = enumToDB(temp);
    }

    {
        int temp;
        if (!parms.ReadAndVerify(
                ActIndex.key, &temp, ActIndex.def, kActionStrings, nActions,
                kObsoleteActions, nObsoleteActions)) {
            return false;
        }

        // TODO:  fix this when settings are really externalized
        const_cast<int&>(mActionIndex) = temp;
    }
    // TODO:  fix this when settings are really externalized
    const_cast<double&>(mThresholdDB) = myThreshold;
    return true;
}

// Effect implementation

double TruncSilenceBase::CalcPreviewInputLength(
    const EffectSettings&, double /* previewLength */) const
{
    double inputLength = mT1 - mT0;
    double minInputLength = inputLength;

    // Master list of silent regions
    RegionList silences;

    // Start with the whole selection silent
    silences.push_back(Region(mT0, mT1));

    int whichTrack = 0;

    for (auto wt : inputTracks()->Selected<const WaveTrack>()) {
        RegionList trackSilences;

        auto index = wt->TimeToLongSamples(mT0);
        sampleCount silentFrame = 0; // length of the current silence

        Analyze(
            silences, trackSilences, *wt, &silentFrame, &index, whichTrack,
            &inputLength, &minInputLength);

        whichTrack += wt->NChannels();
    }
    return inputLength;
}

bool TruncSilenceBase::Process(EffectInstance&, EffectSettings&)
{
    const bool success = mbIndependent ? ProcessIndependently() : ProcessAll();

    return success;
}

bool TruncSilenceBase::ProcessIndependently()
{
    unsigned nGroups = 0;

    // Check if it's permissible
    {
        nGroups = inputTracks()->Selected<const WaveTrack>().size();
    }

    if (nGroups == 0) {
        // nothing to do
        return true;
    }

    // Now do the work

    // Copy tracks
    EffectOutputTracks outputs {
        *mTracks, GetType(), { { mT0, mT1 } }
    };
    double newT1 = 0.0;

    {
        unsigned iGroup = 0;
        for (auto track : outputs.Get().Selected<WaveTrack>()) {
            RegionList silences;
            if (!FindSilences(
                    silences, TrackList::SingletonRange(&as_const(*track)))) {
                return false;
            }
            auto range = TrackList::SingletonRange<Track>(track);
            double totalCutLen = 0.0;
            if (!DoRemoval(silences, range, iGroup, nGroups, totalCutLen)) {
                return false;
            }
            newT1 = std::max(newT1, mT1 - totalCutLen);

            ++iGroup;
        }
    }

    mT1 = newT1;

    outputs.Commit();

    return true;
}

bool TruncSilenceBase::ProcessAll()
{
    // Copy tracks
    EffectOutputTracks outputs {
        *mTracks, GetType(), { { mT0, mT1 } }
    };

    // Master list of silent regions.
    // This list should always be kept in order.
    RegionList silences;

    if (FindSilences(silences, outputs.Get().Selected<const WaveTrack>())) {
        auto trackRange = outputs.Get().Any();
        double totalCutLen = 0.0;
        if (DoRemoval(silences, trackRange, 0, 1, totalCutLen)) {
            mT1 -= totalCutLen;
            outputs.Commit();
            return true;
        }
    }

    return false;
}

bool TruncSilenceBase::FindSilences(
    RegionList& silences, const TrackIterRange<const WaveTrack>& range)
{
    // Start with the whole selection silent
    silences.push_back(Region(mT0, mT1));

    // Remove non-silent regions in each track
    int whichTrack = 0;
    for (auto wt : range) {
        // Smallest silent region to detect in frames
        auto minSilenceFrames = sampleCount(
            std::max(mInitialAllowedSilence, DEF_MinTruncMs) * wt->GetRate());

        //
        // Scan the track for silences
        //
        RegionList trackSilences;

        auto index = wt->TimeToLongSamples(mT0);
        sampleCount silentFrame = 0;

        // Detect silences
        bool cancelled = !(Analyze(
                               silences, trackSilences, *wt, &silentFrame, &index, whichTrack));

        // Buffer has been freed, so we're OK to return if cancelled
        if (cancelled) {
            return false;
        }

        if (silentFrame >= minSilenceFrames) {
            // Track ended in silence -- record region
            trackSilences.push_back(Region(
                                        wt->LongSamplesToTime(index - silentFrame),
                                        wt->LongSamplesToTime(index)));
        }

        // Intersect with the overall silent region list
        Intersect(silences, trackSilences);
        whichTrack++;
    }

    return true;
}

bool TruncSilenceBase::DoRemoval(
    const RegionList& silences, const TrackIterRange<Track>& range,
    unsigned iGroup, unsigned nGroups, double& totalCutLen)
{
    //
    // Now remove the silent regions from all selected
    // tracks.
    //

    // Loop over detected regions in reverse (so cuts don't change time values
    // down the line)
    int whichReg = 0;
    RegionList::const_reverse_iterator rit;
    for (rit = silences.rbegin(); rit != silences.rend(); ++rit) {
        const Region& region = *rit;
        const Region* const r = &region;

        // Progress dialog and cancellation. Do additional cleanup before return.
        const double frac
            =detectFrac + (1 - detectFrac)
              * (iGroup + whichReg / double(silences.size()))
              / nGroups;
        if (TotalProgress(frac)) {
            return false;
        }

        // Intersection may create regions smaller than allowed; ignore them.
        // Allow one nanosecond extra for consistent results with exact
        // milliseconds of allowed silence.
        if ((r->end - r->start) < (mInitialAllowedSilence - 0.000000001)) {
            continue;
        }

        // Find NEW silence length as requested
        double inLength = r->end - r->start;
        double outLength;

        switch (mActionIndex) {
        case kTruncate:
            outLength = std::min(mTruncLongestAllowedSilence, inLength);
            break;
        case kCompress:
            outLength
                =mInitialAllowedSilence + (inLength - mInitialAllowedSilence)
                  * mSilenceCompressPercent / 100.0;
            break;
        default: // Not currently used.
            outLength = std::min(
                mInitialAllowedSilence + (inLength - mInitialAllowedSilence)
                * mSilenceCompressPercent / 100.0,
                mTruncLongestAllowedSilence);
        }

        const double cutLen = std::max(0.0, inLength - outLength);
        // Don't waste time cutting nothing.
        if (cutLen == 0.0) {
            continue;
        }

        totalCutLen += cutLen;

        bool success = true;
        double cutStart = (r->start + r->end - cutLen) / 2;
        double cutEnd = cutStart + cutLen;
        (range + &Track::IsSelected
         -[&](const Track * pTrack) {
            return
            // Don't waste time past the end of a track
            pTrack->GetEndTime() < r->start;
        })
        .VisitWhile(
            success,
            [&](WaveTrack& wt) {
            // In WaveTracks, clear with a cross-fade
            auto blendFrames = mBlendFrameCount;
            // Round start/end times to frame boundaries
            cutStart = wt.SnapToSample(cutStart);
            cutEnd = wt.SnapToSample(cutEnd);

            // Make sure the cross-fade does not affect non-silent frames
            if (wt.LongSamplesToTime(blendFrames) > inLength) {
                // Result is not more than blendFrames:
                blendFrames = wt.TimeToLongSamples(inLength).as_size_t();
            }

            // Perform cross-fade in memory
            struct Buffers
            {
                Buffers(size_t size) : buf1 { size },
                buf2 { size }
                {
                }
                Floats buf1, buf2;
            };
            Buffers buffers[2] { blendFrames, blendFrames };
            auto t1 = wt.TimeToLongSamples(cutStart) - blendFrames / 2;
            auto t2 = wt.TimeToLongSamples(cutEnd) - blendFrames / 2;

            size_t iChannel = 0;
            for (const auto pChannel : wt.Channels()) {
                auto& buffer = buffers[iChannel];
                pChannel->GetFloats(buffer.buf1.get(), t1, blendFrames);
                pChannel->GetFloats(buffer.buf2.get(), t2, blendFrames);

                for (decltype(blendFrames) i = 0; i < blendFrames; ++i) {
                    buffer.buf1[i] = ((blendFrames - i) * buffer.buf1[i]
                                      + i * buffer.buf2[i])
                                     / (double)blendFrames;
                }
                ++iChannel;
            }

            wt.Clear(cutStart, cutEnd, true);

            iChannel = 0;
            for (const auto pChannel : wt.Channels()) {
                // Write cross-faded data
                auto& buffer = buffers[iChannel];
                success
                    =success
                      && pChannel->SetFloats(
                          buffer.buf1.get(), t1, blendFrames,
                          // This effect mostly shifts samples to remove silences,
                          // and does only a little bit of floating point
                          // calculations to cross-fade the splices, over a 100
                          // sample interval by default. Don't dither.
                          narrowestSampleFormat);
                ++iChannel;
            }
        });
        if (!success) {
            return false;
        }
        ++whichReg;
    }

    return true;
}

bool TruncSilenceBase::Analyze(
    RegionList& silenceList, RegionList& trackSilences, const WaveTrack& wt,
    sampleCount* silentFrame, sampleCount* index, int whichTrack,
    double* inputLength, double* minInputLength) const
{
    const auto rate = wt.GetRate();

    // Smallest silent region to detect in frames
    auto minSilenceFrames
        =sampleCount(std::max(mInitialAllowedSilence, DEF_MinTruncMs) * rate);

    double truncDbSilenceThreshold = DB_TO_LINEAR(mThresholdDB);
    auto blockLen = wt.GetMaxBlockSize();
    auto start = wt.TimeToLongSamples(mT0);
    auto end = wt.TimeToLongSamples(mT1);
    sampleCount outLength = 0;

    double previewLength;
    gPrefs->Read(wxT("/AudioIO/EffectsPreviewLen"), &previewLength, 6.0);
    // Minimum required length in samples.
    const sampleCount previewLen(previewLength * rate);

    // Keep position in overall silences list for optimization
    RegionList::iterator rit(silenceList.begin());

    // Allocate buffers
    Floats buffers[] { Floats { blockLen }, Floats { blockLen } };

    // Loop through current track
    while (*index < end)
    {
        if (
            inputLength
            && ((outLength >= previewLen)
                || (*index - start > wt.TimeToLongSamples(*minInputLength)))) {
            *inputLength = std::min<double>(*inputLength, *minInputLength);
            if (outLength >= previewLen) {
                *minInputLength = *inputLength;
            }
            return true;
        }

        if (!inputLength) {
            // Show progress dialog, test for cancellation
            bool cancelled = TotalProgress(
                detectFrac
                * (whichTrack
                   + (*index - start).as_double() / (end - start).as_double())
                / (double)GetNumWaveTracks());
            if (cancelled) {
                return false;
            }
        }

        // Optimization: if not in a silent region skip ahead to the next one

        double curTime = wt.LongSamplesToTime(*index);
        for (; rit != silenceList.end(); ++rit) {
            // Find the first silent region ending after current time
            if (rit->end >= curTime) {
                break;
            }
        }

        if (rit == silenceList.end()) {
            // No more regions -- no need to process the rest of the track
            if (inputLength) {
                // Add available samples up to previewLength.
                auto remainingTrackSamples
                    =wt.TimeToLongSamples(wt.GetEndTime()) - *index;
                auto requiredTrackSamples = previewLen - outLength;
                outLength += (remainingTrackSamples > requiredTrackSamples)
                             ? requiredTrackSamples
                             : remainingTrackSamples;
            }

            break;
        } else if (rit->start > curTime) {
            // End current silent region, skip ahead
            if (*silentFrame >= minSilenceFrames) {
                trackSilences.push_back(Region(
                                            wt.LongSamplesToTime(*index - *silentFrame),
                                            wt.LongSamplesToTime(*index)));
            }
            *silentFrame = 0;
            auto newIndex = wt.TimeToLongSamples(rit->start);
            if (inputLength) {
                auto requiredTrackSamples = previewLen - outLength;
                // Add non-silent sample to outLength
                outLength += ((newIndex - *index) > requiredTrackSamples)
                             ? requiredTrackSamples
                             : newIndex - *index;
            }

            *index = newIndex;
        }
        // End of optimization

        // Limit size of current block if we've reached the end
        auto count = limitSampleBufferSize(blockLen, end - *index);

        // Fill buffers
        size_t iChannel = 0;
        for (const auto pChannel : wt.Channels()) {
            pChannel->GetFloats(buffers[iChannel++].get(), *index, count);
        }

        // Look for silenceList in current block
        for (decltype(count) i = 0; i < count; ++i) {
            if (
                inputLength
                && ((outLength >= previewLen)
                    || (outLength > wt.TimeToLongSamples(*minInputLength)))) {
                *inputLength
                    =wt.LongSamplesToTime(*index + i) - wt.LongSamplesToTime(start);
                break;
            }

            const bool silent
                =std::all_of(buffers, buffers + iChannel, [&](const Floats& buffer) {
                return fabs(buffer[i]) < truncDbSilenceThreshold;
            });
            if (silent) {
                (*silentFrame)++;
            } else {
                sampleCount allowed = 0;
                if (*silentFrame >= minSilenceFrames) {
                    if (inputLength) {
                        switch (mActionIndex) {
                        case kTruncate:
                            outLength
                                +=wt.TimeToLongSamples(mTruncLongestAllowedSilence);
                            break;
                        case kCompress:
                            allowed = wt.TimeToLongSamples(mInitialAllowedSilence);
                            outLength += sampleCount(
                                allowed.as_double()
                                + (*silentFrame - allowed).as_double()
                                * mSilenceCompressPercent / 100.0);
                            break;
                            // default: // Not currently used.
                        }
                    }

                    // Record the silent region
                    trackSilences.push_back(Region(
                                                wt.LongSamplesToTime(*index + i - *silentFrame),
                                                wt.LongSamplesToTime(*index + i)));
                } else if (inputLength) { // included as part of non-silence
                    outLength += *silentFrame;
                }
                *silentFrame = 0;
                if (inputLength) {
                    ++outLength; // Add non-silent sample to outLength
                }
            }
        }
        // Next block
        *index += count;
    }

    if (inputLength) {
        *inputLength = std::min<double>(*inputLength, *minInputLength);
        if (outLength >= previewLen) {
            *minInputLength = *inputLength;
        }
    }

    return true;
}

// EffectTruncSilence implementation

// Finds the intersection of the ordered region lists, stores in dest
void TruncSilenceBase::Intersect(RegionList& dest, const RegionList& src)
{
    RegionList::iterator destIter;
    destIter = dest.begin();
    // Any time we reach the end of the dest list we're finished
    if (destIter == dest.end()) {
        return;
    }
    RegionList::iterator curDest = destIter;

    // Operation: find non-silent regions in src, remove them from dest.
    double nsStart = curDest->start;
    double nsEnd;
    bool lastRun = false; // must run the loop one extra time

    RegionList::const_iterator srcIter = src.begin();

    // This logic, causing the loop to run once after end of src, must occur
    // each time srcIter is updated
    if (srcIter == src.end()) {
        lastRun = true;
    }

    while (srcIter != src.end() || lastRun)
    {
        // Don't use curSrc unless lastRun is false!
        RegionList::const_iterator curSrc;

        if (lastRun) {
            // The last non-silent region extends as far as possible
            nsEnd = std::numeric_limits<double>::max();
        } else {
            curSrc = srcIter;
            nsEnd = curSrc->start;
        }

        if (nsEnd > nsStart) {
            // Increment through dest until we have a region that could be affected
            while (curDest->end <= nsStart)
            {
                ++destIter;
                if (destIter == dest.end()) {
                    return;
                }
                curDest = destIter;
            }

            // Check for splitting dest region in two
            if (nsStart > curDest->start && nsEnd < curDest->end) {
                // The second region
                Region r(nsEnd, curDest->end);

                // The first region
                curDest->end = nsStart;

                // Insert second region after first
                RegionList::iterator nextIt(destIter);
                ++nextIt;

                // This should just read: destIter = dest.insert(nextIt, r); but we
                // work around two two wxList::insert() bugs. First, in some
                // versions it returns the wrong value. Second, in some versions,
                // it crashes when you insert at list end.
                if (nextIt == dest.end()) {
                    dest.push_back(r);
                } else {
                    dest.insert(nextIt, r);
                }
                ++destIter; // (now points at the newly-inserted region)

                curDest = destIter;
            }

            // Check for truncating the end of dest region
            if (
                nsStart > curDest->start && nsStart < curDest->end
                && nsEnd >= curDest->end) {
                curDest->end = nsStart;

                ++destIter;
                if (destIter == dest.end()) {
                    return;
                }
                curDest = destIter;
            }

            // Check for all dest regions that need to be removed completely
            while (nsStart <= curDest->start && nsEnd >= curDest->end)
            {
                destIter = dest.erase(destIter);
                if (destIter == dest.end()) {
                    return;
                }
                curDest = destIter;
            }

            // Check for truncating the beginning of dest region
            if (
                nsStart <= curDest->start && nsEnd > curDest->start
                && nsEnd < curDest->end) {
                curDest->start = nsEnd;
            }
        }

        if (lastRun) {
            // done
            lastRun = false;
        } else {
            // Next non-silent region starts at the end of this silent region
            nsStart = curSrc->end;
            ++srcIter;
            if (srcIter == src.end()) {
                lastRun = true;
            }
        }
    }
}

/*
void TruncSilenceBase::BlendFrames(float* buffer, int blendFrameCount, int
leftIndex, int rightIndex)
{
   float* bufOutput = &buffer[leftIndex];
   float* bufBefore = &buffer[leftIndex];
   float* bufAfter  = &buffer[rightIndex];
   double beforeFactor = 1.0;
   double afterFactor  = 0.0;
   double adjFactor = 1.0 / (double)blendFrameCount;
   for (int j = 0; j < blendFrameCount; ++j)
   {
      bufOutput[j] = (float)((bufBefore[j] * beforeFactor) + (bufAfter[j] *
afterFactor)); beforeFactor -= adjFactor; afterFactor  += adjFactor;
   }
}
*/

bool TruncSilenceBase::NeedsDither() const
{
    return false;
}
